/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using DaveSexton.DocProject.Engine;
using System.IO;
using System.Xml;
using DaveSexton.DocProject.Sandcastle.TopicManagement;

namespace DaveSexton.DocProject.Sandcastle
{
	internal sealed class DynamicFiltersBuildStep : BuildStep<SandcastleBuildEngine>
	{
		#region Public Properties
		public override string Name
		{
			get
			{
				return Resources.Text.ApplyDynamicFiltersBuildStepName;
			}
		}
		#endregion

		#region Private / Protected
		private readonly string filtersFile, reflectionFile, targetfile;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="DynamicFiltersBuildStep" /> class.
		/// </summary>
		public DynamicFiltersBuildStep(SandcastleBuildEngine engine, string reflectionFile, string targetfile, string filtersFile)
			: base(engine)
		{
			if (string.IsNullOrEmpty(reflectionFile))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "reflectionFile");

			if (string.IsNullOrEmpty(targetfile))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "targetfile");

			if (string.IsNullOrEmpty(filtersFile))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "filtersFile");

			this.reflectionFile = reflectionFile;
			this.targetfile = targetfile;
			this.filtersFile = filtersFile;

			RunInBackground = true;
		}
		#endregion

		#region Methods
		protected override void ExecuteInternal(BuildContext context)
		{
			string lastDirectory = Environment.CurrentDirectory;
			Environment.CurrentDirectory = context.CurrentDirectory;

			try
			{
				ApplyFilters(context);
			}
			finally
			{
				Environment.CurrentDirectory = lastDirectory;
			}
		}

		private void ApplyFilters(BuildContext context)
		{
			context.TraceLine();

			TopicFilterCollection filters = null;

			try
			{
				filters = TopicFilter.LoadFilters(filtersFile);
			}
			catch (DirectoryNotFoundException)
			{
				context.AddWarningToList("Missing dynamic topic filters file: " + filtersFile, true);
			}
			catch (FileNotFoundException)
			{
				context.AddWarningToList("Missing dynamic topic filters file: " + filtersFile, true);
			}

			if (filters == null)
				context.TraceLine("There are no filters defined or the file does not exist: " + filtersFile);
			else
			{
				TopicFilterCollection autoFilters = new TopicFilterCollection();

				foreach (ITopicFilter filter in filters)
				{
					if (filter.AutoApply)
						autoFilters.Add(filter);
				}

				int filtersCount = autoFilters.Count;

				if (filtersCount == 0)
				{
					context.TraceLine("There are no filters configured to be applied during builds: " + filtersFile);

					File.Copy(reflectionFile, targetfile, true);
				}
				else
				{
					if (context.Canceled)
						return;

					context.TraceLine("Applying {0} dynamic topic filter(s) defined in: {1}", filtersCount, filtersFile);

					string tempFile = Path.Combine(Path.GetDirectoryName(reflectionFile), "temp_reflection.xml");

					XmlWriterSettings settings = new XmlWriterSettings();
					settings.Indent = true;

					using (XmlWriter writer = XmlWriter.Create(tempFile, settings))
					{
						writer.WriteStartDocument();

						using (ReflectionDocument document = new ReflectionDocument(context.ProjectName, reflectionFile))
						{
							int excludedCount = ApplyFilters(context, document, autoFilters, writer);

							if (!context.Canceled)
							{
								context.TraceLine();
								context.TraceLine("Excluded {0} topic(s).", excludedCount);
							}
						}

						writer.WriteEndDocument();
					}

					File.Move(tempFile, targetfile);
				}
			}
		}

		private static int ApplyFilters(BuildContext context, ReflectionDocument document, TopicFilterCollection filters, XmlWriter writer)
		{
			document.ReadAndWriteToApiElements(writer);

			List<ReflectionApiElement> included = new List<ReflectionApiElement>(1024);
			List<ReflectionApiElement> excluded = new List<ReflectionApiElement>(1024);

			try
			{
				document.ForEachApiElement(delegate(ReflectionApiElement api)
				{
					if (context.Canceled)
						throw new OperationCanceledException();

					bool include = true;
					ITopicFilter excludedBy = null;

					// NOTE: The inclusion state set by one filter can be overwritten by subsequent filters
					foreach (ITopicFilter filter in filters)
					{
						if (filter.IsMatch(api))
						{
							include = filter.Include;

							if (!include)
								excludedBy = filter;
						}
					}

					if (include)
						included.Add(api);
					else
					{
						excluded.Add(api);
						context.TraceLine("Excluded topic \"{0}\" by filter \"{1}\".", api.Title, excludedBy.Name);
					}
				});

				// the first run handles API containment; e.g., namespaces -> topics, topics -> members
				ApplyDependencyFilter(context, included, excluded);
				
				included.Reverse();

				// the next run handles general topic containment; e.g., all members -> members, method overloads -> methods
				ApplyDependencyFilter(context, included, excluded);

				foreach (ReflectionApiElement api in included)
				{
					writer.WriteRaw(Environment.NewLine + "    ");
					api.Write(writer);
				}
			}
			catch (OperationCanceledException) { }

			writer.WriteEndElement();		// </apis>
			writer.WriteRaw(Environment.NewLine);
			writer.WriteEndElement();		// </reflection>

			return excluded.Count;
		}

		private static void ApplyDependencyFilter(BuildContext context, List<ReflectionApiElement> included, List<ReflectionApiElement> excluded)
		{
			for (int i = 0; i < included.Count;)
			{
				ReflectionApiElement api = included[i];

				if (context.Canceled)
					break;

				ReflectionApiElement dependency;

				if (null == (dependency = excluded.Find(delegate(ReflectionApiElement excludedApi)
				{
					return api.IsDependentOn(excludedApi, true);
				})))
				{
					i++;
				}
				else
				{
					context.TraceLine("Excluded topic \"{0}\" by dependency \"{1}\".", api.Title, dependency.Title);

					excluded.Add(api);
					included.Remove(api);
					i--;
				}
			}
		}

		protected override void CancelInternal(BuildContext context)
		{
			// do nothing
		}
		#endregion
	}
}
